namespace NTrace
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Security.Cryptography;
    using System.Text;
    using System.Text.RegularExpressions;

    public class TraceMessageFile
    {
        private static Regex formatSpecifiers = new Regex(
          "(?<!{){(?<argumentIndex>\\d+)}",
          RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);

        private PreprocessorContext context;
        private String mungedSourceFileName;
        private Guid messageGuid;
        private int nextMessageNumber = 10;
        private StringBuilder buffer;

        internal TraceMessageFile(PreprocessorContext context)
        {
            System.Diagnostics.Debug.Assert(context != null, "Expected a non-null trace context.");
            if (context == null) throw new ArgumentNullException("context");

            this.context = context;
            this.mungedSourceFileName = System.IO.Path.GetFileName(this.context.CodeFile).ToLower().Replace('.', '_');
            this.messageGuid = this.CalculateMessageGuid();
            this.buffer = new StringBuilder();

            this.WriteTraceMetadataFiles();
            this.AddHeader();
            String traceMessageFileName = System.IO.Path.ChangeExtension(
              this.messageGuid.ToString("B"),
              ".tmf");
        }

        internal Guid MessageGuid
        {
            get { return messageGuid; }
        }

        internal void Write()
        {
            String fileName = String.Format("{0:D}.tmf", this.messageGuid);
            using (System.IO.StreamWriter writer = new System.IO.StreamWriter(
              System.IO.Path.Combine(
                this.context.Environment.OutputFolder,
                fileName)))
            {
                writer.Write(this.buffer.ToString());
            }
        }

        private Guid CalculateMessageGuid()
        {
            using (MD5 sha1 = MD5.Create())
            {
                using (System.IO.Stream fileContents = new FileStream(this.context.CodeFile, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    byte[] hash = sha1.ComputeHash(fileContents);
                    return new Guid(hash);
                }
            }
        }

        private void WriteTraceMetadataFiles()
        {
            String upperCaseID = String.Format("{0:D}", this.context.Environment.ProviderId).ToUpper();

            this.WriteTraceMessageControlFile(upperCaseID);

            WriteControlGuidsFile(upperCaseID);
        }

        private void WriteTraceMessageControlFile(String upperCaseID)
        {
            String traceMessageControlFileName = String.Format("{0:D}.tmc", this.context.Environment.ProviderId);
            System.Collections.Generic.SortedDictionary<int, String> sortedFlags = new SortedDictionary<int, String>();
            foreach (String flagName in this.context.Environment.Configuration.TraceFlags.Keys)
            {
                sortedFlags[this.context.Environment.Configuration.TraceFlags[flagName]] = flagName;
            }

            using (System.IO.StreamWriter writer = new System.IO.StreamWriter(
              System.IO.Path.Combine(
                this.context.Environment.OutputFolder,
                traceMessageControlFileName)))
            {
                writer.WriteLine("//PDB:  ");

                writer.WriteLine(
                  "//PDB:  Last Updated: {0:U} UTC by {1}",
                DateTime.Now,
                System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase));

                writer.WriteLine(upperCaseID);

                writer.WriteLine(this.context.Environment.FriendlyName);

                foreach (int flagValue in sortedFlags.Keys)
                {
                    writer.WriteLine(sortedFlags[flagValue]);
                }
            }
        }

        private void WriteControlGuidsFile(String upperCaseID)
        {
            String controlGuidFileName = String.Format("{0:D}.ctl", this.context.Environment.ProviderId);
            using (System.IO.StreamWriter writer = new System.IO.StreamWriter(
              System.IO.Path.Combine(
                this.context.Environment.OutputFolder,
                controlGuidFileName)))
            {
                writer.Write(upperCaseID);
                writer.Write(" \"");
                writer.Write(this.context.Environment.FriendlyName);
                writer.WriteLine("\"");
            }
        }

        private void AddHeader()
        {
            this.buffer.AppendLine("//PDB:  ");

            this.buffer.AppendFormat(
              "//PDB:  Last Updated: {0:U} UTC by {1}",
              DateTime.Now,
              System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase));

            this.buffer.AppendLine();

            this.buffer.AppendFormat(
              "{0:D} {1} // SRC={2} MJ= MN=",
              this.messageGuid,
              this.context.Environment.FriendlyName,
              System.IO.Path.GetFileName(this.context.CodeFile));

            this.buffer.AppendLine();
        }

        internal int AddMessage(
          int line,
          String message,
          int flag,
          int level,
          IList<String> arguments,
          String fullMethodName,
          bool staticMethod)
        {
            const Int32 BaseOffset = 10; // user-defined messages start at 10
            Int32 argumentOffset = BaseOffset;
            //#typev  <<file>><<line>> <<messagenumber>> "%0 [<<string>>[%<<10-based parameter number>>!s!]...]" //   LEVEL=TL_INFO FLAGS=TF_DIAG FUNC=<<Fully-qualified method name>>
            //{
            //[arg[0...], ItemPWString -- <<parameter number>>]...
            //}

            // This is a new message, so increment the current message number
            int messageNumber = this.nextMessageNumber++;

            // Count the number of format string arguments
            // Walk the list of format specifiers
            StringBuilder formatString = new StringBuilder();
            int lastIndex = 0;

            if (!staticMethod)
            {
                // Add the hash code placeholder
                formatString.Append("(%10!p!) ");
                argumentOffset++;
            }

            Match specifier = formatSpecifiers.Match(message, lastIndex);
            while (specifier != null && specifier.Success)
            {
                formatString.Append(message, lastIndex, specifier.Index - lastIndex);

                Int32 argumentIndex = Int32.Parse(specifier.Groups["argumentIndex"].Value);

                if (argumentIndex > arguments.Count - 1) throw new PreprocessorException(
                  PreprocessorErrors.TooManyFormatStringSpecifiers,
                  "Format string argument index is greater than the number of supplied arguments.")
                  {
                      File = this.context.CodeFile,
                      StartLine = line,
                      StartColumn = 0,
                      EndLine = line,
                      EndColumn = 0
                  };

                argumentIndex += argumentOffset;
                formatString.AppendFormat("%{0}!s!", argumentIndex);

                lastIndex = specifier.Index + specifier.Length;
                specifier = specifier.NextMatch();
            }
            formatString.Append(message.Substring(lastIndex));

            this.buffer.AppendFormat(
              "#typev {0}{1} {2} \"%0",
              this.mungedSourceFileName,
              line,
              messageNumber);

            this.buffer.Append(formatString);
            this.buffer.AppendFormat(
              "\" //   LEVEL={0} FLAGS={1} FUNC={2}",
              level, // TODO: Get TL_XX string
              flag, // TODO: Get flag string
              fullMethodName);

            this.buffer.AppendLine();

            this.buffer.AppendLine("{");

            if (!staticMethod)
            {
                // BUGBUG: We are currently passing GetHashCode as a string argument
                //this.buffer.AppendLine("    this.GetHashCode(), ItemPtr -- 10");
                this.buffer.AppendLine("    this.GetHashCode(), ItemPWString -- 10");
            }

            for (int index = 0; index < arguments.Count; index++)
            {
                this.buffer.AppendFormat("    {0}, ItemPWString -- {1}\n", arguments[index], index + argumentOffset);
                this.buffer.AppendLine();
            }

            this.buffer.AppendLine("}");

            return messageNumber;
        }
    }
}
